From 3484787e9a5b499e7f2b3cd87b8c53f8e9555cae Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Sun, 16 Nov 2025 13:47:10 +0100 Subject: [PATCH] all: implement RFC8910 captive portal (CP) option MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit https://www.rfc-editor.org/rfc/rfc8910.html RFC8908 defines a captive portal client (CPC) that handles the API URI returned. The captive portal (CP) option presence signals a portal that requires authentication beyond what odhcp6c currently handles. It's possible that a user connecting through an openwrt gateway encounters the portal anyway (though this behaviour depends on the portal), but if this is not the case, with this addition we can: - surface a message in a UI as to presence of the CP - signal that users install a CPC (a chicken/egg problem behind a CP) - provide the API URI for downstream consumers. This should ease the use of travel router scenarios. Downstream consumers of the API URI can find it via the CAPTIVE_PORTAL_URI environment/ubus property. The strengths of having this option handled means downstream consumers get one unified environment variable since ra.c does not yet handle CUSTOM_* the way DHCPv6 does. Signed-off-by: Paul Donald Link: https://github.com/openwrt/odhcp6c/pull/127 Signed-off-by: Álvaro Fernández Rojas --- README.md | 1 + src/dhcpv6.c | 25 +++++++++++++++++++++++++ src/odhcp6c.c | 1 + src/odhcp6c.h | 7 +++++++ src/ra.c | 29 +++++++++++++++++++++++++++++ src/ra.h | 8 ++++++++ src/script.c | 29 ++++++++++++++++++++++++++++- src/ubus.c | 4 +++- 8 files changed, 102 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5397cea..121dffd 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ The script is called with the following parameters: `` `` | `RA_RETRANSMIT` | ND Retransmit time | | `AFTR` | The DS-Lite AFTR domain name | | `MAPE` / `MAPT` / `LW4O6` | Softwire rules for MAPE, MAPT and LW4O6 | +| `CAPTIVE_PORTAL_URI` | RFC8910 captive portal API URI received from upstream | | `PASSTHRU` | The content of the last packet relayed | diff --git a/src/dhcpv6.c b/src/dhcpv6.c index 4077e0f..71b163c 100644 --- a/src/dhcpv6.c +++ b/src/dhcpv6.c @@ -617,6 +617,8 @@ int init_dhcpv6(const char *ifname) htons(DHCPV6_OPT_SNTP_SERVERS), htons(DHCPV6_OPT_NTP_SERVER), htons(DHCPV6_OPT_PD_EXCLUDE), + /* RFC8910: Clients that support this option SHOULD include it */ + htons(DHCPV6_OPT_CAPTIVE_PORTAL), }; odhcp6c_add_state(STATE_ORO, oro, sizeof(oro)); } @@ -1370,6 +1372,7 @@ static int dhcpv6_handle_reply(enum dhcpv6_msg orig, _o_unused const int rc, odhcp6c_clear_state(STATE_S46_MAPT); odhcp6c_clear_state(STATE_S46_MAPE); odhcp6c_clear_state(STATE_S46_LW); + odhcp6c_clear_state(STATE_CAPT_PORT); odhcp6c_clear_state(STATE_PASSTHRU); odhcp6c_clear_state(STATE_CUSTOM_OPTS); @@ -1530,6 +1533,28 @@ static int dhcpv6_handle_reply(enum dhcpv6_msg orig, _o_unused const int rc, odhcp6c_add_state(STATE_S46_LW, odata, olen); break; + case DHCPV6_OPT_CAPTIVE_PORTAL: /* RFC8910 §2.2 */ + size_t ref_len = sizeof(URN_IETF_CAPT_PORT_UNRESTR) - 1; + /* RFC8910 §2: + * Networks with no captive portals may explicitly indicate this + * condition by using this option with the IANA-assigned URI for + * this purpose. Clients observing the URI value ... may forego + * time-consuming forms of captive portal detection. */ + if (memcmp(odata, URN_IETF_CAPT_PORT_UNRESTR, ref_len)) { + /* RFC8910 §2.2: + * Note that the URI parameter is not null terminated. + * Allocate new buffer including room for '\0' */ + size_t uri_len = olen + 1; + uint8_t *copy = malloc(uri_len); + if (!copy) + continue; + memcpy(copy, odata, olen); + copy[uri_len] = '\0'; + odhcp6c_add_state(STATE_CAPT_PORT, odata, olen); + free(copy); + } + break; + default: odhcp6c_add_state(STATE_CUSTOM_OPTS, &odata[-DHCPV6_OPT_HDR_SIZE], olen + DHCPV6_OPT_HDR_SIZE); break; diff --git a/src/odhcp6c.c b/src/odhcp6c.c index a8e826a..cda9ef4 100644 --- a/src/odhcp6c.c +++ b/src/odhcp6c.c @@ -510,6 +510,7 @@ int main(_o_unused int argc, char* const argv[]) odhcp6c_clear_state(STATE_NTP_FQDN); odhcp6c_clear_state(STATE_SIP_IP); odhcp6c_clear_state(STATE_SIP_FQDN); + odhcp6c_clear_state(STATE_CAPT_PORT); bound = false; size_t oro_len = 0; diff --git a/src/odhcp6c.h b/src/odhcp6c.h index b0094fe..0bd686c 100644 --- a/src/odhcp6c.h +++ b/src/odhcp6c.h @@ -70,6 +70,10 @@ #define RA_MIN_ADV_INTERVAL 3 /* RFC 4861 paragraph 6.2.1 */ +/* RFC8910 §2 */ +static const uint8_t URN_IETF_CAPT_PORT_UNRESTR[] = "urn:ietf:params:capport:unrestricted"; +#define CAPT_PORT_URI_STR "CAPTIVE_PORTAL_URI" + enum dhcvp6_opt { /* RFC8415(bis) */ DHCPV6_OPT_CLIENTID = 1, @@ -155,6 +159,8 @@ enum dhcvp6_opt { DHCPV6_OPT_LQ_BASE_TIME = 100, DHCPV6_OPT_LQ_START_TIME = 101, DHCPV6_OPT_LQ_END_TIME = 102, + /* RFC8910 */ + DHCPV6_OPT_CAPTIVE_PORTAL = 103, /* RFC7839 */ DHCPV6_OPT_ANI_ATT = 105, DHCPV6_OPT_ANI_NETWORK_NAME = 106, @@ -429,6 +435,7 @@ enum odhcp6c_state { STATE_S46_MAPT, STATE_S46_MAPE, STATE_S46_LW, + STATE_CAPT_PORT, STATE_PASSTHRU, _STATE_MAX }; diff --git a/src/ra.c b/src/ra.c index b1011be..d44b9f0 100644 --- a/src/ra.c +++ b/src/ra.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -557,6 +558,34 @@ bool ra_process(void) ra_holdoff_interval); entry->auxlen = 0; } + } else if (opt->type == ND_OPT_CAPTIVE_PORTAL) { + /* RFC8910 Captive-Portal §2.3 */ + if (opt->len <= 1) + continue; + + struct icmpv6_opt_captive_portal *capt_port = (struct icmpv6_opt_captive_portal*)opt; + uint8_t *buf = &capt_port->data[0]; + size_t ref_len = sizeof(URN_IETF_CAPT_PORT_UNRESTR) - 1; + + /* RFC8910 §2: + * Networks with no captive portals may explicitly indicate this + * condition by using this option with the IANA-assigned URI for + * this purpose. Clients observing the URI value ... may forego + * time-consuming forms of captive portal detection. */ + if (memcmp(buf, URN_IETF_CAPT_PORT_UNRESTR, ref_len)) { + /* URI are not guaranteed to be \0 terminated if data is unpadded */ + size_t uri_len = (capt_port->len * 8) - 2; + /* Allocate new buffer including room for '\0' */ + uint8_t *copy = malloc(uri_len + 1); + if (!copy) + continue; + + memcpy(copy, buf, uri_len); + copy[uri_len] = '\0'; + odhcp6c_clear_state(STATE_CAPT_PORT); + odhcp6c_add_state(STATE_CAPT_PORT, copy, uri_len); + free(copy); + } } } diff --git a/src/ra.h b/src/ra.h index a869b5e..6f6f59c 100644 --- a/src/ra.h +++ b/src/ra.h @@ -32,6 +32,13 @@ struct icmpv6_opt { uint8_t data[6]; }; +/* RFC8910 Captive-Portal §2.3 */ +struct icmpv6_opt_captive_portal { + uint8_t type; /* 37 */ + uint8_t len; /* includes the Type and Length fields, in units of 8 bytes */ + uint8_t data[]; /* padded with NUL (0x00) to make length multiple of 8 */ +}; + struct icmpv6_opt_route_info { uint8_t type; uint8_t len; @@ -42,6 +49,7 @@ struct icmpv6_opt_route_info { }; #define ND_OPT_ROUTE_INFORMATION 24 +#define ND_OPT_CAPTIVE_PORTAL 37 #define icmpv6_for_each_option(opt, start, end)\ diff --git a/src/script.c b/src/script.c index 10c3482..5a6168d 100644 --- a/src/script.c +++ b/src/script.c @@ -142,6 +142,31 @@ static void fqdn_to_env(const char *name, const uint8_t *fqdn, size_t len) putenv(buf); } +static void string_to_env(const char *name, const uint8_t *string, size_t len) +{ + size_t buf_len = strlen(name); + const uint8_t *string_end = string + len; + char *buf = realloc(NULL, len + buf_len + 2); + + memcpy(buf, name, buf_len); + buf[buf_len++] = '='; + + while (string < string_end) { + int l = strlen((const char *)string); + if (l <= 0) + break; + string += l; + buf_len += strlen(&buf[buf_len]); + buf[buf_len++] = ' '; + } + + if (buf[buf_len - 1] == ' ') + buf_len--; + + buf[buf_len] = '\0'; + putenv(buf); +} + static void bin_to_env(uint8_t *opts, size_t len) { uint8_t *oend = opts + len, *odata; @@ -436,7 +461,7 @@ void script_call(const char *status, int delay, bool resume) } else if (pid == 0) { size_t dns_len, search_len, custom_len, sntp_ip_len, ntp_ip_len, ntp_dns_len; size_t sip_ip_len, sip_fqdn_len, aftr_name_len, addr_len; - size_t s46_mapt_len, s46_mape_len, s46_lw_len, passthru_len; + size_t s46_mapt_len, s46_mape_len, s46_lw_len, capt_port_len, passthru_len; signal(SIGTERM, SIG_DFL); if (delay > 0) { @@ -457,6 +482,7 @@ void script_call(const char *status, int delay, bool resume) uint8_t *s46_mapt = odhcp6c_get_state(STATE_S46_MAPT, &s46_mapt_len); uint8_t *s46_mape = odhcp6c_get_state(STATE_S46_MAPE, &s46_mape_len); uint8_t *s46_lw = odhcp6c_get_state(STATE_S46_LW, &s46_lw_len); + uint8_t *capt_port = odhcp6c_get_state(STATE_CAPT_PORT, &capt_port_len); uint8_t *passthru = odhcp6c_get_state(STATE_PASSTHRU, &passthru_len); size_t prefix_len, address_len, ra_pref_len, @@ -480,6 +506,7 @@ void script_call(const char *status, int delay, bool resume) s46_to_env(STATE_S46_MAPE, s46_mape, s46_mape_len); s46_to_env(STATE_S46_MAPT, s46_mapt, s46_mapt_len); s46_to_env(STATE_S46_LW, s46_lw, s46_lw_len); + string_to_env(CAPT_PORT_URI_STR, capt_port, capt_port_len); bin_to_env(custom, custom_len); if (odhcp6c_is_bound()) { diff --git a/src/ubus.c b/src/ubus.c index 664feff..ab2e362 100644 --- a/src/ubus.c +++ b/src/ubus.c @@ -531,7 +531,7 @@ static int states_to_blob(void) char *buf = NULL; size_t dns_len, search_len, custom_len, sntp_ip_len, ntp_ip_len, ntp_dns_len; size_t sip_ip_len, sip_fqdn_len, aftr_name_len, addr_len; - size_t s46_mapt_len, s46_mape_len, s46_lw_len, passthru_len; + size_t s46_mapt_len, s46_mape_len, s46_lw_len, capt_port_len, passthru_len; struct in6_addr *addr = odhcp6c_get_state(STATE_SERVER_ADDR, &addr_len); struct in6_addr *dns = odhcp6c_get_state(STATE_DNS, &dns_len); uint8_t *search = odhcp6c_get_state(STATE_SEARCH, &search_len); @@ -545,6 +545,7 @@ static int states_to_blob(void) uint8_t *s46_mapt = odhcp6c_get_state(STATE_S46_MAPT, &s46_mapt_len); uint8_t *s46_mape = odhcp6c_get_state(STATE_S46_MAPE, &s46_mape_len); uint8_t *s46_lw = odhcp6c_get_state(STATE_S46_LW, &s46_lw_len); + uint8_t *capt_port = odhcp6c_get_state(STATE_CAPT_PORT, &capt_port_len); uint8_t *passthru = odhcp6c_get_state(STATE_PASSTHRU, &passthru_len); size_t prefix_len, address_len, ra_pref_len, @@ -572,6 +573,7 @@ static int states_to_blob(void) CHECK(s46_to_blob(STATE_S46_MAPE, s46_mape, s46_mape_len)); CHECK(s46_to_blob(STATE_S46_MAPT, s46_mapt, s46_mapt_len)); CHECK(s46_to_blob(STATE_S46_LW, s46_lw, s46_lw_len)); + blobmsg_add_string(&b, CAPT_PORT_URI_STR, (char *)capt_port); CHECK(bin_to_blob(custom, custom_len)); if (odhcp6c_is_bound()) { -- 2.30.2